Skip to main content

Classes

Python is a class-based inheritance language. In simple terms, classes are blueprints to create objects.

class Dog:
def __init__(self, name, age):
self.name = name
self.age = age

def bark(self):
return f"{self.name} says Woof!"

# Creating a dog object
my_dog = Dog("Buddy", 3)
print(my_dog.bark()) # Output: Buddy says Woof!

I. Core Class Components​

1. __init__ method (Constructor)​

The __init__ method is Python’s special initialization method (a constructor). When you create a new object from a class, Python automatically calls this method to set up the initial state of the object.

my_dog = Dog("Buddy", 3)

→ When Python executes this line, it:

  • Creates a new empty object
  • Calls __init__ with the provided arguments
  • Returns the initialized object

2. self and Instance Attributes​

In Python, self represents the specific instance of the class that’s being worked with. It's how the code knows which object's data to access or modify. Every instance method in a class automatically receives the instance (self) as the first parameter.

def bark(self):
return f"{self.name} says Woof!"

Example:

class Library:
def __init__(self, name):
self.name = name
self.books = [] # Each library has its own book list

def add_book(self, book):
# self.books refers to THIS library's book list
self.books.append(book)

def book_count(self):
# Returns the count for THIS specific library
return len(self.books)

# Creating two different libraries
city_library = Library("City Library")
school_library = Library("School Library")

# Adding books to different libraries
city_library.add_book(gatsby) # Adds to city_library's books list
school_library.add_book(hobbit) # Adds to school_library's books list

# Each library has its own separate count
print(city_library.book_count()) # 1
print(school_library.book_count()) # 1

While it's convention to use the name self, Python cares about the position, not the name. This code would work the same way:

class Library:
def __init__(this_library, name): # "this_library" instead of "self"
this_library.name = name
this_library.books = []

3. Method Types​

a. Instance Methods​

Regular methods that access instance data

  • First parameter is self
  • Can access/modify instance attributes

b. Class Methods​

Methods that work with class variables.

@classmethod
def from_birth_year(cls, name, year):
return cls(name, 2024 - year)

c. Static Methods​

Utility functions that don’t access instance/class data.

@staticmethod
def is_valid_name(name):
return bool(name.strip())
**Example: **
class Student:
# Class variable shared by all instances
school_name = "Python High"
student_count = 0

def __init__(self, name, age):
self._name = name
self._age = age
Student.student_count += 1

# Instance Method
# - Uses self to access instance data
# - Can modify instance state
def get_info(self):
return f"{self._name} is {self._age} years old"

# Class Method
# - Uses cls instead of self
# - Can access/modify class state
# - Can create class instances
@classmethod
def from_birth_year(cls, name, birth_year):
age = 2024 - birth_year
return cls(name, age)

@classmethod
def get_school_info(cls):
return f"{cls.school_name} has {cls.student_count} students"

# Static Method
# - Doesn't use self or cls
# - Utility function related to class purpose
# - Can't access instance or class state
@staticmethod
def is_valid_age(age):
return 0 <= age <= 120

@staticmethod
def calculate_grade_average(grades):
return sum(grades) / len(grades) if grades else 0

# Using different method types
# 1. Instance Method
student = Student("Alice", 20)
print(student.get_info()) # "Alice is 20 years old"

# 2. Class Methods
# Creating instance using alternate constructor
bob = Student.from_birth_year("Bob", 2000)
print(Student.get_school_info()) # "Python High has 2 students"

# 3. Static Methods
# Utility functions that don't need instance/class data
print(Student.is_valid_age(25)) # True
print(Student.calculate_grade_average([85, 90, 95])) # 90.0

4. Class Variables vs. Instance Variables​

**Example: **
class Library:
# Class variable - shared by ALL instances
total_books = 0

def __init__(self, name):
# Instance variables - unique to each instance
self._name = name
self._books = []

def add_book(self, book):
self._books.append(book)
# Updating the class variable
Library.total_books += 1

@property
def book_count(self):
return len(self._books)

@property
def books(self):
# Return a copy to prevent direct modification
return self._books.copy()

# Using the class
city_lib = Library("City")
school_lib = Library("School")

city_lib.add_book("1984")
school_lib.add_book("Hobbit")

print(city_lib.book_count) # 1
print(Library.total_books) # 2 (tracks books across ALL libraries)

II. Data Protection & Access Control​

1. Encapsulation​

Python uses naming conventions for different levels of attribute access. Python doesn’t enforce protection - it’s a convention.

1. Public Attributes (self.name)

  • Accessible from anywhere outside the class
  • No special naming convention
  • Used when you want the attribute to be part of the class’s public interface

2. Protected Attributes (self._name)

  • Single underscore prefix
  • It is still technically accessible, but signals “internal use only”. This is a naming convention telling other developers “don’t access this directly”.

3. Private Attributes (self.__name)

  • Double underscore prefix
class Dog:
def __init__(self, name, age):

self.public = "Anyone can access" # Public attribute
self._protected = "Don't access directly" # Protected attribute
self.__private = "Hidden" # Private attribute (name mangled)

2. Properties (Getter and Setters)​

Properties provide controlled access to class attributes, allowing validation and computed values.

a. Getters​

Getter: A method decorated with @property that returns a private attribute's value.

  • Controls how private data is accessed
  • Can compute values on-the-fly

b. Setters​

Setter: A method decorated with @property_name.setter that sets a private attribute's value.

  • Controls how private data is modified
  • Enables data validation
  • Can transform input before storage
  • Optional - omitting it makes the property read-only
**Example: **
class Dog:
def __init__(self, name, birth_year):
self._name = name
self._birth_year = birth_year

@property
def age(self):
# Age is computed from birth year
return 2025 - self._birth_year

@property
def human_age(self):
# Computed property based on another property
return self.age * 7

# Using computed properties
my_dog = Dog("Buddy", 2020)
print(my_dog.age) # 5 (computed from birth year)
print(my_dog.human_age) # 35 (computed from age)
  1. Data Validation
@property
def age(self):
return self.__age

@age.setter
def age(self, value):
if value < 0:
raise ValueError("Age cannot be negative")
self.__age = value
  1. Computed Values
@property
def full_name(self):
return f"{self.__first_name} {self.__last_name}"

III. Inheritance & Advanced Concepts​

1. Basic Inheritance​

Inheritance allows creating specialized versions of classes:

class Animal:
def __init__(self, name, age):
self._name = name
self._age = age

@property
def name(self):
return self._name

class Dog(Animal): # Dog inherits from Animal
def __init__(self, name, age, breed):
super().__init__(name, age) # Call parent class's __init__
self._breed = breed

@property
def name(self): # Override parent's property
return f"{self._name} the {self._breed}"

2. Multiple Inheritance​

class Device:
def turn_on(self):
print("Device on")

class Radio:
def play_music(self):
print("Playing music")

class SmartRadio(Device, Radio): # Inherits from both classes
pass

# Can use methods from both parent classes
radio = SmartRadio()
radio.turn_on() # From Device
radio.play_music() # From Radio

3. Abstract Classes​

Abstract classes are like blueprints for other classes. They define a common interface that all subclasses must implement.

from abc import ABC, abstractmethod

# Abstract base class for all animals
class Animal(ABC):
def __init__(self, name):
self.name = name

# Every animal must make a sound, but each does it differently
@abstractmethod
def make_sound(self):
pass

# A regular method all animals can use as-is
def introduce(self):
return f"I am {self.name}"

# A concrete class implementing Animal
class Cat(Animal):
def make_sound(self):
return "Meow!"

# Another concrete class
class Dog(Animal):
def make_sound(self):
return "Woof!"

# Using the classes
cat = Cat("Whiskers")
dog = Dog("Buddy")

print(cat.introduce()) # "I am Whiskers"
print(cat.make_sound()) # "Meow!"
print(dog.make_sound()) # "Woof!"

# This would raise an error - can't create abstract class
# my_animal = Animal("Generic") # Error!